__all__ = ['parse_command_line', 'log_config', 'init', 'schedule_tasks', 'clean']


import argparse
import logging
import os
import pypdf
import re
import sys
import types

import util


__author__  = 'ZL <837806295@qq.com>'
__product__ = 'outlinepdf'
__version__ = '1.2.0'
__date__    = '2023/05/14'


log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())  # 如果宿主未配置日志处理器，则丢弃日志

args = types.SimpleNamespace()

password = None

outline_suffix = '_outline.txt'


def get_image_infos(path):
    """使用OCR库从图片中提取信息
    """

    if not isinstance(path, str) or not path or not os.path.isfile(path):
        log.critical('get image infos ... failed ... exit | illegal input')
        sys.exit(1)

    try:
        from cnocr import CnOcr

        ocr = CnOcr(det_model_name='naive_det')
        infos = ocr.ocr(path)
    except Exception:
        log.exception('get image infos ... exception ... exit')
        sys.exit(1)

    log.debug('get image infos ... successed | infos: %s', infos)
    return infos

def save_image_infos(infos, path):
    """保存提取到的文本信息
    """

    if (not isinstance(infos, list)
            or not infos
            or not isinstance(path, str)
            or not path):
        log.critical('save image infos ... failed ... exit | illegal input')
        sys.exit(1)

    try:
        with open(path, 'a', encoding='utf-8') as f:
            for info in infos:
                f.write(info['text'])
                f.write('\n')
    except Exception:
        log.exception('save image infos ... exception ... exit')
        sys.exit(1)

    log.debug('save image infos ... successed')
    return True

def get_title_from_outline_field(field):
    """从pdf的目录项域提取标题
    """

    if field is None:
        log.warning('get title from outline field ... ongoing | none')
        return ''

    try:
        title = str(field)
    except Exception:
        log.exception('get title from outline field ... exception')
        return ''

    return title

def get_page_from_outline_field(reader, field):
    """从pdf的目录项域提取页码
    """

    if field is None:
        log.warning('get page from outline field ... ongoing | none')
        return 0

    try:
        page = reader.get_page_number(field)
    except Exception:
        log.exception('get page from outline field ... exception')
        return 0

    try:
        page = int(page)
    except Exception:
        log.exception('get page from outline field ... exception')
        return 0

    return page

def get_fit_info_from_outline_field(field, cls):
    """从pdf的目录项域提取页面显示配置信息
    """

    if field is None:
        log.warning('get fit info from outline field ... ongoing | none')
        return None

    try:
        fit_info = cls(field)
    except Exception:
        log.exception('get fit info from outline field ... exception')
        return None

    return fit_info

def get_font_format_from_outline_field(field):
    """从pdf的目录项域提取字体格式
    """

    if field is None:
        log.warning('get font format from outline field ... ongoing | none')
        return None

    try:
        font_format = int(field)
    except Exception:
        log.exception('get font format from outline field ... exception')
        return None

    if font_format < 0 or font_format > 3:
        return None

    return font_format

def get_color_from_outline_field(field):
    """从pdf的目录项域提取颜色
    """

    if field is None:
        log.warning('get color from outline field ... ongoing | none')
        return None

    try:
        color = [float(f) for f in field]
    except Exception:
        log.exception('get color from outline field ... exception')
        return None

    return color

def export_pdf_content_item(reader, destination):
    """把Destination转换为上下文无关的dict
    """

    if (not isinstance(reader, pypdf.PdfReader)
            or not isinstance(destination, pypdf.generic.Destination)):
        log.critical('export pdf content item ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    log.debug('export pdf content item ... ongoing | title: %s',
              destination.title)

    title = get_title_from_outline_field(destination.title)
    page = get_page_from_outline_field(reader, destination.page)
    fit_type = get_fit_info_from_outline_field(destination.typ, str)
    fit_zoom = get_fit_info_from_outline_field(destination.zoom, int)
    fit_left = get_fit_info_from_outline_field(destination.left, float)
    fit_top = get_fit_info_from_outline_field(destination.top, float)
    fit_right = get_fit_info_from_outline_field(destination.right, float)
    fit_bottom = get_fit_info_from_outline_field(destination.bottom, float)
    font_format = get_font_format_from_outline_field(destination.font_format)
    color = get_color_from_outline_field(destination.color)

    item = {
        'title': title,
        'page': page,
        'fit_type': fit_type,
        'fit_zoom': fit_zoom,
        'fit_left': fit_left,
        'fit_top': fit_top,
        'fit_right': fit_right,
        'fit_bottom': fit_bottom,
        'font_format': font_format,
        'color': color
    }

    log.debug('export pdf content item ... successed | item: %s', item)
    return item

def export_pdf_sub_content(reader, destinations):
    """把Destination的list转换为上下文无关的dict的list
    """

    if (not isinstance(reader, pypdf.PdfReader)
            or not isinstance(destinations, list)
            or not destinations):
        log.critical('export pdf sub content ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    parent = None
    sub_content = []

    for item in destinations:
        log.debug('export pdf sub content ... ongoing | item: %s', item)

        if isinstance(item, pypdf.generic.Destination):
            parent = export_pdf_content_item(reader, item)
            sub_content.append(parent)
        elif isinstance(item, list):
            if not isinstance(parent, dict):
                log.critical('export pdf sub content ... failed | parent')
                sys.exit(1)

            sub_content2 = export_pdf_sub_content(reader, item)
            if sub_content2:
                parent['sub_content'] = sub_content2
        else:
            log.critical('export pdf sub content ... failed')
            sys.exit(1)

    log.debug('export pdf sub content ... successed | sub content: %s',
              sub_content)
    return sub_content

def export_pdf_content(path):
    """导出pdf文档的书签

    书签是一个list，它的项或者是一个Destination，或者是一个Destination的list，每层的直接
    子项就是一个Destination，直接子项的子项会紧跟着这个子项以Destination的list呈现，如此
    层层递进
    """

    if not isinstance(path, str) or not path or not os.path.isfile(path):
        log.critical('export pdf content ... failed ... exit | illegal input')
        sys.exit(1)

    reader = pypdf.PdfReader(path)

    outline = reader.outline
    log.debug('export pdf content ... ongoing | outline: %s', outline)

    if not isinstance(outline, list) or not outline:
        log.warning('export pdf content ... ignored | nonexistence')
        return None

    content = export_pdf_sub_content(reader, outline)

    log.debug('export pdf content ... successed | content: %s', content)
    return content

def normalize_title(title, replace_rules):
    """对标题进行规范化
    """

    if not isinstance(title, str) or not isinstance(replace_rules, list):
        log.critical('normalize title ... failed ... exit | illegal input')
        sys.exit(1)

    for rule in replace_rules:
        if (isinstance(rule, list)
                and len(rule) >= 2
                and isinstance(rule[0], str)
                and isinstance(rule[1], str)):
            title = re.sub(rule[0], rule[1], title)

    if not title:
        log.warning('normalize title ... ongoing | empty')

    return title

def convert_pdf_content_item_to_line(level, item, replace_rules=[]):
    """把一项目录内容转换为文本行

    用TAB缩进表示层级，标题、页码、适用模式、字体格式和字体颜色用TAB隔开，适用模式用冒号隔开
    键值，字体颜色是空格隔开的三个浮点数
    """

    if (not isinstance(level, int)
            or not isinstance(item, dict)
            or not isinstance(replace_rules, list)):
        log.critical('convert pdf content item to line ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    try:
        log.debug('convert pdf content item to line ... ongoing | title: %s',
                  item['title'])

        level_tab = '\t' * level

        title = normalize_title(item['title'], replace_rules)
        if not title:
            title = 'None'

        page = item['page']
        if not isinstance(page, int) or page < 0:
            page = 0

        fit = ''

        if item['fit_type'] is not None:
            fit += 'type: %s' % item['fit_type']

        if item['fit_zoom'] is not None:
            fit += ' zoom: %s' % item['fit_zoom']

        if item['fit_left'] is not None:
            fit += ' left: %s' % item['fit_left']

        if item['fit_top'] is not None:
            fit += ' top: %s' % item['fit_top']

        if item['fit_right'] is not None:
            fit += ' right: %s' % item['fit_right']

        if item['fit_bottom'] is not None:
            fit += ' bottom: %s' % item['fit_bottom']

        if not fit:
            fit = 'None'

        font_format = item['font_format']
        if (not isinstance(font_format, int)
                or font_format < 0
                or font_format > 3):
            font_format = 0

        if not isinstance(item['color'], list) or len(item['color']) < 3:
            color = '0.0 0.0 0.0'
        else:
            color = ' '.join([str(item['color'][i]) for i in range(3)])

        if not color:
            color = '0.0 0.0 0.0'

        line = ('%s%s\t%d\t%s\t%s\t%s\n'
                % (level_tab,
                   title,
                   page,
                   fit,
                   font_format,
                   color))
    except Exception:
        log.exception('convert pdf content item to line ... exception')
        return None

    log.debug('convert pdf content item to line ... successed | line: %s', line)
    return line

def convert_pdf_sub_content_to_lines(level, items, replace_rules=[]):
    """把子目录内容转为文本行
    """

    if (not isinstance(level, int)
            or not isinstance(items, list)
            or not items
            or not isinstance(replace_rules, list)):
        log.critical('convert pdf sub content to lines ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    lines = ''

    for item in items:
        log.debug('convert pdf sub content to lines ... ongoing | item: %s',
                  item)

        if not isinstance(item, dict):
            log.error('convert pdf sub content to lines ... failed')
            return None

        line = convert_pdf_content_item_to_line(level, item, replace_rules)
        if not line:
            log.error('convert pdf sub content to lines ... failed')
            return None

        lines += line

        if ('sub_content' in item.keys()
                and isinstance(item['sub_content'], list)
                and item['sub_content']):
            lines2 = convert_pdf_sub_content_to_lines(level + 1,
                                                      item['sub_content'],
                                                      replace_rules)
            if not lines2:
                log.error('convert pdf sub content to lines ... failed')
                return None

            lines += lines2

    log.debug('convert pdf sub content to lines ... successed | lines: %s',
              lines)
    return lines

def save_pdf_content(path, content, replace_rules=[]):
    """保存导出的pdf书签到文本文档
    """

    if (not isinstance(path, str)
            or not path
            or not isinstance(content, list)
            or not content):
        log.critical('save pdf content ... failed ... exit | illegal input')
        sys.exit(1)

    lines = convert_pdf_sub_content_to_lines(0, content, replace_rules)
    if not lines:
        log.error('save pdf content ... failed')
        return False

    try:
        with open(path, 'w', encoding='utf-8') as f:
            f.write(lines)
    except Exception:
        log.exception('save pdf content ... exception')
        return False

    log.debug('save pdf content ... successed')
    return True

def get_line_level(line):
    """获取行的缩进层级
    """

    if not isinstance(line, str) or not line:
        log.critical('get line level ... failed ... exit | illegal input')
        sys.exit(1)

    level = 0
    while len(line) > level and line[level] == '\t':
        level += 1

    log.debug('get line level ... successed | level: %d', level)
    return level

def get_parent_by_level(parents, level):
    """获取所在缩进层级项的父亲项的子目录list
    """

    if (not isinstance(parents, list)
            or not parents
            or not isinstance(level, int)
            or level < 0
            or level >= len(parents)):
        log.critical('get parent by level ... failed ... exit | illegal input')
        sys.exit(1)

    if level == 0:
        parent = parents[level]
        if not isinstance(parent, list):
            log.critical('get parent by level ... failed ... exit | parent')
            sys.exit(1)
    else:
        parent = parents[level]
        if not isinstance(parent, dict):
            log.critical('get parent by level ... failed ... exit | parent')
            sys.exit(1)

        if 'sub_content' not in parent.keys():
            parent['sub_content'] = []

        parent = parent['sub_content']

    log.debug('get parent by level ... successed | parent: %s', parent)
    return parent

def get_fields_from_line(line):
    """从行内容中获取由TAB隔开的各个域

    例如：
    "abc"                 -> ["abc"]
    " abc\tdef"           -> [" abc", "def"]
    "\tabc\t def\t"       -> ["abc", " def"]
    "abc\t\tdef "         -> ["abc", "def "]
    "\t\tabc \t\tdef\t\t" -> ["abc ", "def"]
    """

    if not isinstance(line, str) or not line:
        log.critical('get fields from line ... failed ... exit | illegal input')
        sys.exit(1)

    fields = []
    pos = 0
    tab_count = 0
    non_tab_count = 0

    for i in range(len(line)):
        if line[i] == '\t':
            tab_count += 1
            non_tab_count = 0
        else:
            tab_count = 0
            non_tab_count += 1

        if tab_count == 1 and i != 0:
            fields.append(line[pos : i])

        if non_tab_count == 1:
            pos = i

    if line[-1] != '\t':
        fields.append(line[pos :])

    log.debug('get fields from line ... successed | fields: %s', fields)
    return fields

def get_title_from_field(field, replace_rules=[]):
    """从标题所在域提取标题

    如果提供了正则替换规则，会依次执行这些替换规则。要确保处理后的标题不为空串
    """

    title = field.strip(' ')
    title = normalize_title(title, replace_rules)

    if not title:
        log.critical('get title from field ... failed ... exit')
        sys.exit(1)

    return title

def get_page_from_field(field):
    """从页码所在域提取页码

    要确保能够提取到正常页码
    """

    page = field.strip(' ')

    try:
        page = int(page)
    except Exception:
        log.exception('get page from field ... exception ... exit')
        sys.exit(1)

    if page < 0:
        log.critical('get page from field ... failed ... exit')
        sys.exit(1)

    return page

def get_fit_info_from_field(field, key, cls):
    """从页面显示模式配置所在域提取页面显示配置信息

    提取不到合法信息就用None标记
    """

    fit = field.strip(' ')

    fit_info = None

    m = re.search('%s%s' % (key, r'\:\s*([^\s]+)\s+'), fit)
    if m and m.group and m.group(1):
        fit_info = m.group(1)

    if fit_info is None:
        m = re.search('%s%s' % (key, r'\:\s*([^\s]+)$'), fit)
        if m and m.group and m.group(1):
            fit_info = m.group(1)

    if fit_info is None:
        return None

    try:
        fit_info = cls(fit_info)
    except Exception:
        log.exception('get fit info from field ... exception')
        return None

    return fit_info

def get_font_format_from_field(field):
    """从字体格式所在域提取字体格式

    提取不到合法字体格式就用None标记
    """

    font_format = field.strip(' ')

    try:
        font_format = int(font_format)
    except Exception:
        log.exception('get font format from field ... exception')
        return None

    if font_format < 0 or font_format > 3:
        return None

    return font_format

def get_color_from_field(field):
    """从颜色所在域提取颜色

    提取不到合法颜色就用None标记
    """

    field = field.strip(' ')

    color = None

    m = re.search(r'^([\d.]+)\s+([\d.]+)\s+([\d.]+)$', field)
    if m and m.group and m.group(1) and m.group(2) and m.group(3):
        color = [m.group(1), m.group(2), m.group(3)]

    if color is None:
        return None

    try:
        color = [float(f) for f in color]
    except Exception:
        log.exception('get color from field ... exception')
        return None

    return color

def get_content_item_from_line(line,
                               replace_rules=[],
                               adjust_page=[],
                               fit='type: /FitH',
                               font_format=0,
                               color=[0.0, 0.0, 0.0]):
    """从行内容中获取目录项信息
    """

    if (not isinstance(line, str)
            or not line
            or not isinstance(replace_rules, list)
            or not isinstance(adjust_page, list)
            or not isinstance(fit, str)
            or not isinstance(font_format, int)
            or not isinstance(color, list)
            or len(color) < 3):
        log.critical('get content item from line ... failed ... exit | '
                     'illegal input')
        sys.exit(1)

    log.debug('get content item from line ... ongoing | line: %s', line)

    content_item = {
        'title': None,
        'page': None,
        'fit_type': None,
        'fit_zoom': None,
        'fit_left': None,
        'fit_top': None,
        'fit_right': None,
        'fit_bottom': None,
        'font_format': None,
        'color': None
    }

    fields = get_fields_from_line(line)
    if len(fields) < 2:
        log.critical('get content item from line ... failed ... exit | field')
        sys.exit(1)

    content_item['title'] = get_title_from_field(fields[0], replace_rules)

    page2 = page = get_page_from_field(fields[1])
    for pair in adjust_page:
        if (isinstance(pair, tuple)
                and len(pair) > 1
                and isinstance(pair[0], int)
                and isinstance(pair[1], int)
                and page >= pair[0]):
            page2 += pair[1]
    if page2 < 0:
        log.warning('get content item from line ... ongoing | page: %d, '
                    'adjusted page: %d', page, page2)
        page = 0
    else:
        page = page2

    content_item['page'] = page

    if len(fields) >= 3:
        fit = fields[2]

    content_item['fit_type'] = get_fit_info_from_field(fit, 'type', str)
    content_item['fit_zoom'] = get_fit_info_from_field(fit, 'zoom', int)
    content_item['fit_left'] = get_fit_info_from_field(fit, 'left', float)
    content_item['fit_top'] = get_fit_info_from_field(fit, 'top', float)
    content_item['fit_right'] = get_fit_info_from_field(fit, 'right', float)
    content_item['fit_bottom'] = get_fit_info_from_field(fit, 'bottom', float)

    if len(fields) >= 4:
        font_format = get_font_format_from_field(fields[3])

    content_item['font_format'] = font_format

    if len(fields) >= 5:
        color = get_color_from_field(fields[4])

    content_item['color'] = color

    log.debug('get content item from line ... successed | content item: %s',
              content_item)
    return content_item

def get_content_from_lines(lines,
                           replace_rules=[],
                           adjust_page=[],
                           fit='type: /FitH',
                           font_format=0,
                           color=[0.0, 0.0, 0.0]):
    """分析行以得到pdf书签
    """

    if (not isinstance(lines, list)
            or not isinstance(replace_rules, list)
            or not isinstance(adjust_page, list)
            or not isinstance(fit, str)
            or not isinstance(font_format, int)
            or not isinstance(color, list)
            or len(color) < 3):
        log.critical('get content from lines ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    if not lines:
        log.warning('get content from lines ... ongoing | empty')
        return []

    content = []
    parents = [content]
    last_level = -1

    for line in lines:
        if not line:
            log.warning('get content from lines ... ongoing | empty')
            continue

        if not line.lstrip('\t'):
            log.warning('get content from lines ... ongoing | whitespace')
            continue

        log.debug('get content from lines ... ongoing | line: %s', line)

        level = get_line_level(line)
        if level < 0 or level > last_level + 1:
            log.critical('get content from lines ... failed ... exit | level')
            sys.exit(1)
        else:
            last_level = level

        item = get_content_item_from_line(line.lstrip('\t'),
                                          replace_rules,
                                          adjust_page,
                                          fit,
                                          font_format,
                                          color)

        if 0 < level + 1 < len(parents):
            parents[level + 1] = item
        elif level + 1 == len(parents):
            parents.append(item)
        else:
            log.critical('get content from lines ... failed ... exit | level')
            sys.exit(1)

        parent = get_parent_by_level(parents, level)
        parent.append(item)

    log.debug('get content from lines ... successed | content: %s', content)
    return content

def load_pdf_content(path,
                     replace_rules=[],
                     adjust_page=[],
                     fit='type: /FitH',
                     font_format=0,
                     color=[0.0, 0.0, 0.0]):
    """从文本文档中加载pdf书签
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(replace_rules, list)
            or not isinstance(adjust_page, list)
            or not isinstance(fit, str)
            or not isinstance(font_format, int)
            or not isinstance(color, list)
            or len(color) < 3):
        log.critical('load pdf content ... failed ... exit | illegal input')
        sys.exit(1)

    content = []

    try:
        with open(path, 'r', encoding='utf-8') as f:
            lines = [line.rstrip('\n\t ').lstrip(' ') for line in f.readlines()]
            content = get_content_from_lines(lines,
                                             replace_rules,
                                             adjust_page,
                                             fit,
                                             font_format,
                                             color)
    except Exception:
        log.exception('load pdf content ... exception')
        return None

    log.debug('load pdf content ... successed | content: %s', content)
    return content

def generate_fit(type_, zoom, left, top, right, bottom):
    """生成Fit类的对象
    """

    if not isinstance(type_, str) or not type_:
        log.warning('generate fit ... ongoing | type: %s', type_)
        return pypdf.generic.Fit.fit_horizontally()

    fit = None

    try:
        if type_.lower() == '/xyz':
            fit = pypdf.generic.Fit.xyz(left, top, zoom)
        elif type_.lower() == '/fit':
            fit = pypdf.generic.Fit.fit()
        elif type_.lower() == '/fith':
            fit = pypdf.generic.Fit.fit_horizontally(top)
        elif type_.lower() == '/fitv':
            fit = pypdf.generic.Fit.fit_vertically(left)
        elif type_.lower() == '/fitr':
            fit = pypdf.generic.Fit.fit_rectangle(left, bottom, right, top)
        elif type_.lower() == '/fitb':
            fit = pypdf.generic.Fit.fit_box(left, top, zoom)
        elif type_.lower() == '/fitbh':
            fit = pypdf.generic.Fit.fit_box_horizontally(top)
        elif type_.lower() == '/fitbv':
            fit = pypdf.generic.Fit.fit_box_vertically(left)
        else:
            fit = pypdf.generic.Fit.fit_horizontally()
    except Exception:
        log.exception('generate fit ... exception')
        return pypdf.generic.Fit.fit_horizontally()

    log.debug('generate fit ... successed | fit: %s', fit)
    return fit

def import_pdf_content_item(writer, parent, item):
    """导入一个目录项到pdf文件
    """

    if (not isinstance(writer, pypdf.PdfWriter)
            or not isinstance(item, dict)
            or not item):
        log.critical('import pdf content item ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    try:
        log.debug('import pdf content item ... ongoing | title: %s',
                  item['title'])

        title = item['title']
        if not title:
            title = 'None'

        page = item['page']
        if not isinstance(page, int) or page < 0:
            page = 0

        fit = generate_fit(item['fit_type'],
                           item['fit_zoom'],
                           item['fit_left'],
                           item['fit_top'],
                           item['fit_right'],
                           item['fit_bottom'])

        font_format = item['font_format']
        if font_format == 0:
            bold = False
            italic = False
        elif font_format == 1:
            bold = False
            italic = True
        elif font_format == 2:
            bold = True
            italic = False
        elif font_format == 3:
            bold = True
            italic = True
        else:
            bold = False
            italic = False

        color = item['color']
        if isinstance(color, list):
            color = tuple(color)
        else:
            color = (0.0, 0.0, 0.0)
    except Exception:
        log.exception('import pdf content item ... exception')
        return None

    try:
        outline_item = writer.add_outline_item(title,
                                               page,
                                               parent,
                                               None,
                                               color,
                                               bold,
                                               italic,
                                               fit)
    except Exception:
        log.exception('import pdf content item ... exception')
        return None

    log.debug('import pdf content item ... successed')
    return outline_item

def import_pdf_sub_content(writer, parent, items):
    """导入一个子目录到pdf文件
    """

    if (not isinstance(writer, pypdf.PdfWriter)
            or not isinstance(items, list)
            or not items):
        log.critical('import pdf sub content ... failed ... exit | illegal '
                     'input')
        sys.exit(1)

    for item in items:
        log.debug('import pdf sub content ... ongoing | item: %s', item)

        if not isinstance(item, dict):
            log.error('import pdf sub content ... failed')
            return False

        outline_item = import_pdf_content_item(writer, parent, item)
        if outline_item is None:
            log.error('import pdf sub content ... failed')
            return False

        if ('sub_content' in item.keys()
                and isinstance(item['sub_content'], list)
                and item['sub_content']):
            ok = import_pdf_sub_content(writer,
                                        outline_item,
                                        item['sub_content'])
            if not ok:
                log.error('import pdf sub content ... failed')
                return False

    log.debug('import pdf sub content ... successed')
    return True

def import_pdf_content(path, content):
    """导入书签到pdf文档
    """

    if (not isinstance(path, str)
            or not path
            or not os.path.isfile(path)
            or not isinstance(content, list)
            or not content):
        log.critical('import pdf content ... failed ... exit | illegal input')
        sys.exit(1)

    reader = pypdf.PdfReader(path)
    writer = pypdf.PdfWriter()

    for page in reader.pages:
        writer.add_page(page)

    ok = import_pdf_sub_content(writer, None, content)
    if not ok:
        log.error('import pdf content ... failed')
        return False

    root, ext = os.path.splitext(path)
    save_path = '%s%s%s' % (root, '2', ext)

    try:
        with open(save_path, 'wb') as f:
            writer.write(f)
    except Exception:
        log.exception('import pdf content ... exception')
        return False

    log.debug('import pdf content ... successed')
    return True

def merge_pdf_files(paths, out):
    """合并pdf文档
    """

    if (not isinstance(paths, list)
            or not paths
            or not isinstance(out, str)
            or not out):
        log.critical('merge pdf files ... failed ... exit | illegal input')
        sys.exit(1)

    merger = pypdf.PdfWriter()

    try:
        for path in paths:
            merger.append(path)

        merger.write(out)
    except Exception:
        log.exception('merge pdf files ... exception')
        return False
    finally:
        merger.close()

    return True

def import_file():
    log.info('import file ... ongoing')

    if os.path.isfile(args.outline):
        outline = args.outline
    elif os.path.isdir(args.outline):
        _, filename = os.path.split(args.path)
        outline = os.path.join(args.outline,
                               '%s%s' % (filename, outline_suffix))

        if not os.path.isfile(outline):
            util.prompt_status('Load pdf content', 'ignored')
            return
    else:
        util.prompt_status('Load pdf content', 'ignored')
        return

    replace_rules = []
    if args.normalize:
        replace_rules = util.load_config_by_associated_lines(args.normalize)

    content = load_pdf_content(outline,
                               replace_rules,
                               args.adjust_page,
                               args.fit,
                               args.font_format,
                               args.color)
    util.prompt_tristate2(content, 'Load pdf content')

    ok = import_pdf_content(args.path, content)
    util.prompt_assert(ok, 'Import pdf content')

    log.info('import file ... done')

def import_dir():
    log.info('import dir ... ongoing')

    if not os.path.isdir(args.outline):
        util.prompt_status('Import dir', 'ignored')
        return

    paths = util.get_dir_files_by_extension(args.path, True, args.extension)

    replace_rules = []
    if args.normalize:
        replace_rules = util.load_config_by_associated_lines(args.normalize)

    for path in paths:
        util.prompt_status('Import the following file', 'ongoing')
        util.prompt(path, True)

        _, filename = os.path.split(path)
        outline = os.path.join(args.outline,
                               '%s%s' % (filename, outline_suffix))

        if not os.path.isfile(outline):
            util.prompt_status('Load pdf content', 'ignored')
            continue

        content = load_pdf_content(outline,
                                   replace_rules,
                                   args.adjust_page,
                                   args.fit,
                                   args.font_format,
                                   args.color)
        util.prompt_assert2(content, 'Load pdf content', False)

        if not content:
            continue

        ok = import_pdf_content(path, content)
        util.prompt_assert(ok, 'Import pdf content')

    log.info('import dir ... done')

def import_():
    if os.path.isfile(args.path):
        import_file()
    elif os.path.isdir(args.path):
        import_dir()

def export_file():
    log.info('export file ... ongoing')

    content = export_pdf_content(args.path)
    util.prompt_assert2(content, 'Extract pdf content')

    replace_rules = []
    if args.normalize:
        replace_rules = util.load_config_by_associated_lines(args.normalize)

    path = '%s%s' % (args.path, outline_suffix)
    log.info('export file ... ongoing | path: %s, export path: %s',
             args.path,
             path)

    ok = save_pdf_content(path, content, replace_rules)
    util.prompt_assert(ok, 'Save pdf content')

    log.info('export file ... done')

def export_dir():
    log.info('export dir ... ongoing')

    paths = util.get_dir_files_by_extension(args.path, True, args.extension)

    replace_rules = []
    if args.normalize:
        replace_rules = util.load_config_by_associated_lines(args.normalize)

    for path in paths:
        util.prompt_status('Export the following file', 'ongoing')
        util.prompt(path, True)

        content = export_pdf_content(path)
        util.prompt_assert2(content, 'Extract pdf content', False)

        if not content:
            continue

        path2 = '%s%s' % (path, outline_suffix)
        log.info('export dir ... ongoing | path: %s, export path: %s',
                 path,
                 path2)

        ok = save_pdf_content(path2, content, replace_rules)
        util.prompt_assert(ok, 'Save pdf content', False)

    log.info('export dir ... done')

def export():
    if os.path.isfile(args.path):
        export_file()
    elif os.path.isdir(args.path):
        export_dir()

def extract_file():
    log.info('extract file ... ongoing')

    util.prompt_status('Extract info', 'ongoing')
    infos = get_image_infos(args.path)
    util.prompt_assert(infos, 'Extract info')

    ok = save_image_infos(infos, args.outline)
    util.prompt_assert(ok, 'Save info')

    log.info('extract file ... done')

def extract_dir():
    log.info('extract dir ... ongoing')

    paths = util.get_dir_files_by_extension(args.path, True, args.extension)
    paths.sort()

    for path in paths:
        util.prompt_status('Extract the following file', 'ongoing')
        util.prompt(path, True)

        infos = get_image_infos(path)
        util.prompt_assert(infos, 'Extract info')

        ok = save_image_infos(infos, args.outline)
        util.prompt_assert(ok, 'Save info')

    log.info('extract dir ... done')

def extract():
    if os.path.isfile(args.path):
        extract_file()
    elif os.path.isdir(args.path):
        extract_dir()

def merge():
    log.info('merge ... ongoing')

    paths = util.get_dir_files_by_extension(args.path, True, args.extension)
    util.prompt_assert2(paths, 'Get pdf files')

    paths.sort()

    ok = merge_pdf_files(paths, args.out)
    util.prompt_assert(ok, 'Merge pdf files')

    log.info('merge ... done')

def check_args(args, subcommand):
    if args.subcommand == 'import':
        # 处理-a/--adjust_page选项，必须跟偶数个值，转换为两元素元组的列表
        if args.adjust_page is None:
            args.adjust_page = []
        elif isinstance(args.adjust_page, list):
            if len(args.adjust_page) % 2 != 0:
                subcommand.error('arguement -a/--adjust_page: values should be '
                                 'provided in pairs')
            else:
                adjust_page = []
                for i in range(0, len(args.adjust_page), 2):
                    adjust_page.append(
                        (args.adjust_page[i], args.adjust_page[i+1]))
                args.adjust_page = adjust_page

    return args

def parse_command_line():
    parser = argparse.ArgumentParser(
        prog=__product__,
        description='给pdf文件做书签时提供辅助的小工具')

    parser.add_argument('-v', '--version',
                        action='version',
                        version='%(prog)s ' + __version__)

    subparsers = parser.add_subparsers(
        title='subcommands',
        required=True,
        dest='subcommand',
        description='子命令集，由相应的子命令完成相应的功能')

    # import子命令，从特定格式文本文档中导入书签到pdf文件中
    import_ = subparsers.add_parser(
        'import',
        help='从具有特定格式的书签内容的文本文档中导入书签到pdf文件中',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
        ]
    )
    import_.add_argument('path',
                         type=util.arg_type_path,
                         help='与具有特定格式的书签内容的文本文档对应pdf文件的完整路径'
                              '或者包含pdf文件的目录的完整路径')
    import_.add_argument('outline',
                         type=util.arg_type_path,
                         help='待导入的具有特定格式的书签内容的文本文档的完整路径或者'
                              '包含具有特定格式的书签内容的文本文档的目录的完整路径')
    import_.add_argument('-n', '--normalize',
                         type=util.arg_type_file,
                         help='包含进行替换的Python正则表达式对的文本文件的完整路径，用'
                              '以规范化目录项的内容')
    import_.add_argument('-a', '--adjust_page',
                         type=int,
                         nargs='*',
                         default=[],
                         help='若干对整数，用来调整页码。每对第一个数指示从哪页开始要调整'
                              '页码；每对第二个数表示调整值，会用这个数加上原来页码得到新'
                              '页码。默认是空，表示不调整')
    import_.add_argument('-i', '--fit',
                         type=util.arg_type_non_empty,
                         default='type: /FitH',
                         help='当文本文档中的项没有给页面显示模式配置时，会使用这个选项'
                              '的值，是包含空格一串字符，默认是'
                              '“type: /FitH”，也就是宽度适应的页面显示模式')
    import_.add_argument('-f', '--font_format',
                         type=int,
                         choices=[0, 1, 2, 3],
                         default=0,
                         help='当文本文档中的项没有给字体格式时，会使用这个选项的值，0'
                              '是正常，1是斜体，2粗体，3是粗斜体，默认是0，也就是正常')
    import_.add_argument('-c', '--color',
                         type=float,
                         nargs=3,
                         default=[0.0, 0.0, 0.0],
                         help='当文本文档中的项没有给颜色时，3个0.0-1.0之间的浮点数，表'
                              '示RGB颜色，默认3个都是0.0，也就是黑色')
    import_.add_argument('-t', '--extension',
                         type=util.arg_type_non_empty2,
                         nargs='+',
                         default=['.pdf'],
                         help='不分大小写指定pdf文件扩展名，当path选项给的是目录时，只'
                              '会处理有这些扩展名的文件，默认是“.pdf”')

    # export子命令，导出pdf文件书签到特定格式文本文档
    export = subparsers.add_parser(
        'export',
        help='导出pdf文件书签到文本文档',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
        ]
    )
    export.add_argument('path',
                        type=util.arg_type_path,
                        help='待导出书签的pdf文件的完整路径或者待导出书签的pdf文件所在'
                             '目录的完整路径')
    export.add_argument('-n', '--normalize',
                        type=util.arg_type_file,
                        help='包含进行替换的Python正则表达式对的文本文件的完整路径，用'
                             '以规范化目录项的内容')
    export.add_argument('-t', '--extension',
                        type=util.arg_type_non_empty2,
                        nargs='+',
                        default=['.pdf'],
                        help='不分大小写指定pdf文件扩展名，当path选项给的是目录时，只'
                             '会处理有这些扩展名的文件，默认是“.pdf”')

    # extract子命令，从图片中提取目录文字到文本文档，会用到OCR功能
    extract = subparsers.add_parser(
        'extract',
        help='从图片中提取目录文字到文本文档，会用到OCR功能',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
        ]
    )
    extract.add_argument('path',
                         type=util.arg_type_path,
                         help='待提取目录文字的pdf中目录页面截图文件的完整路径或所在目录'
                              '完整路径')
    extract.add_argument('outline',
                         type=util.arg_type_non_empty,
                         help='接收目录文字的文本文档的完整路径')
    extract.add_argument('-t', '--extension',
                         type=util.arg_type_non_empty2,
                         nargs='+',
                         default=['.png', '.jpg', '.jpeg'],
                         help='不分大小写指定图片文件扩展名，当path选项给的是目录时，只'
                              '会处理有这些扩展名的文件，默认是“.png .jpg .jpeg”')

    # merge子命令，合并pdf文档
    merge = subparsers.add_parser(
        'merge',
        help='合并pdf文档',
        parents=[
            util.arg_log(),
            util.arg_log_identifier(),
        ]
    )
    merge.add_argument('path',
                       type=util.arg_type_dir,
                       help='待合并pdf文档所在目录完整路径')
    merge.add_argument('out',
                       type=util.arg_type_non_empty,
                       help='合并后的文件的完整路径')
    merge.add_argument('-t', '--extension',
                       type=util.arg_type_non_empty2,
                       nargs='+',
                       default=['.pdf'],
                       help='不分大小写指定pdf文件扩展名，当path选项给的是目录时，只'
                            '会处理有这些扩展名的文件，默认是“.pdf”')

    subcommands = {
        'import': import_,
        'export': export,
        'extract': extract,
        'merge': merge
    }

    global args
    args = parser.parse_args()
    args = check_args(args, subcommands[args.subcommand])

def log_config():
    if hasattr(args, 'log'):
        if hasattr(args, 'log_identifier') and args.log_identifier:
            return (
                '%s_v%s' % (__product__, __version__),
                args.log,
                args.log_identifier
            )
        else:
            return ('%s_v%s' % (__product__, __version__), args.log)

    return None

def init():
    log.info('program version: %s %s', __product__, __version__)
    log.debug('command line: %s', sys.argv)
    log.debug('parse args: %s', args)

    # 获取操作系统密码以便任务完成后自动关机
    if hasattr(args, 'shutdown'):
        global password
        password = util.get_os_password(args.shutdown)

def schedule_tasks():
    if args.subcommand == 'import':
        import_()
    elif args.subcommand == 'export':
        export()
    elif args.subcommand == 'extract':
        extract()
    elif args.subcommand == 'merge':
        merge()

def clean():
    # 自动关机
    if hasattr(args, 'shutdown'):
        util.shutdown(password)
